package resolver

import (
	"context"
	"math"
	"sync"
	"sync/atomic"
	"time"

	"go-main/library/conf/env"
	"go-main/library/ecode"
	nmd "go-main/library/net/metadata"
	"go-main/library/stat/metric"
)

type md struct {
	weight int64
	color  string
}

type subAddr struct {
	addr string
	meta md

	// effective weight
	ewt int64
	// current weight
	cwt int64

	err     metric.RollingCounter
	latency metric.RollingGauge
	si      serverInfo

	score float64
}

type serverInfo struct {
	cpu     int64
	success uint64 // float64 bits
}

type wrrPicker struct {
	// subConns is the snapshot of the weighted-roundrobin balancer when this picker was
	// created. The slice is immutable. Each Get() will do a round robin
	// selection from it and return the selected SubConn.
	subAddrs []*subAddr
	colors   map[string]*wrrPicker
	updateAt int64

	mu sync.Mutex
}

func (p *wrrPicker) Pick(ctx context.Context) (string, func(err error), error) {
	// FIXME refactor to unify the color logic
	color := nmd.String(ctx, nmd.Color)
	if color == "" && env.Color != "" {
		color = env.Color
	}
	if color != "" {
		if cp, ok := p.colors[color]; ok {
			return cp.pick(ctx)
		}
	}
	return p.pick(ctx)
}

func (p *wrrPicker) pick(ctx context.Context) (string, func(err error), error) {
	var (
		conn        *subAddr
		totalWeight int64
	)
	if len(p.subAddrs) <= 0 {
		return "", nil, ecode.ServiceUnavailable
	}
	p.mu.Lock()
	// nginx wrr load balancing algorithm: http://blog.csdn.net/zhangskd/article/details/50194069
	for _, sc := range p.subAddrs {
		totalWeight += sc.ewt
		sc.cwt += sc.ewt
		if conn == nil || conn.cwt < sc.cwt {
			conn = sc
		}
	}
	conn.cwt -= totalWeight
	p.mu.Unlock()
	start := time.Now()
	// if cmd, ok := nmd.FromContext(ctx); ok {
	// 	cmd["addr"] = conn
	// }
	//if !feature.DefaultGate.Enabled(dwrrFeature) {
	//	return conn.conn, nil, nil
	//}
	return conn.addr, func(err error) {
		ev := int64(0) // error value ,if error set 1
		if err != nil {
			ev = 1
		}
		conn.err.Add(ev)

		now := time.Now()
		conn.latency.Add(now.Sub(start).Nanoseconds() / 1e5)
		u := atomic.LoadInt64(&p.updateAt)
		if now.UnixNano()-u < int64(time.Second) {
			return
		}
		if !atomic.CompareAndSwapInt64(&p.updateAt, u, now.UnixNano()) {
			return
		}
		var (
			count int
			total float64
		)
		for _, conn := range p.subAddrs {
			cpu := float64(atomic.LoadInt64(&conn.si.cpu))
			ss := math.Float64frombits(atomic.LoadUint64(&conn.si.success))
			errc, req := conn.errSummary()
			lagv, lagc := conn.latencySummary()

			if req > 0 && lagc > 0 && lagv > 0 {
				// client-side success ratio
				cs := 1 - (float64(errc) / float64(req))
				if cs <= 0 {
					cs = 0.1
				} else if cs <= 0.2 && req <= 5 {
					cs = 0.2
				}
				conn.score = math.Sqrt((cs * ss * ss * 1e9) / (lagv * cpu))
			}

			if conn.score > 0 {
				total += conn.score
				count++
			}
		}
		// count must be greater than 1,otherwise will lead ewt to 0
		if count < 2 {
			return
		}
		avgscore := total / float64(count)
		p.mu.Lock()
		for _, conn := range p.subAddrs {
			if conn.score <= 0 {
				conn.score = avgscore
			}
			conn.ewt = int64(conn.score * float64(conn.meta.weight))
		}
		p.mu.Unlock()
	}, nil
}

func (c *subAddr) errSummary() (err int64, req int64) {
	c.err.Reduce(func(iterator metric.Iterator) float64 {
		for iterator.Next() {
			bucket := iterator.Bucket()
			req += bucket.Count
			for _, p := range bucket.Points {
				err += int64(p)
			}
		}
		return 0
	})
	return
}

func (c *subAddr) latencySummary() (latency float64, count int64) {
	c.latency.Reduce(func(iterator metric.Iterator) float64 {
		for iterator.Next() {
			bucket := iterator.Bucket()
			count += bucket.Count
			for _, p := range bucket.Points {
				latency += p
			}
		}
		return 0
	})
	return latency / float64(count), count
}

// Build is
func Build(readySAs map[string]*md) *wrrPicker {
	p := &wrrPicker{
		colors: make(map[string]*wrrPicker),
	}
	for addr, meta := range readySAs {
		if meta == nil {
			meta = &md{
				weight: 10,
			}
		}
		subc := &subAddr{
			addr: addr,

			meta:  *meta,
			ewt:   int64(meta.weight),
			score: -1,

			err: metric.NewRollingCounter(metric.RollingCounterOpts{
				Size:           10,
				BucketDuration: time.Millisecond * 100,
			}),
			latency: metric.NewRollingGauge(metric.RollingGaugeOpts{
				Size:           10,
				BucketDuration: time.Millisecond * 100,
			}),
			si: serverInfo{cpu: 500, success: math.Float64bits(1)},
		}
		if meta.color == "" {
			p.subAddrs = append(p.subAddrs, subc)
			continue
		}
		// if color not empty, use color picker
		cp, ok := p.colors[meta.color]
		if !ok {
			cp = &wrrPicker{}
			p.colors[meta.color] = cp
		}
		cp.subAddrs = append(cp.subAddrs, subc)
	}
	return p
}
